/*
 * Copyright (c) 2013 IBM Corporation.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 */
package com.ibm.wala.cast.js.callgraph.fieldbased;

import com.ibm.wala.cast.js.callgraph.fieldbased.flowgraph.FlowGraph;
import com.ibm.wala.cast.js.callgraph.fieldbased.flowgraph.vertices.CallVertex;
import com.ibm.wala.cast.js.callgraph.fieldbased.flowgraph.vertices.FuncVertex;
import com.ibm.wala.cast.js.callgraph.fieldbased.flowgraph.vertices.VarVertex;
import com.ibm.wala.cast.js.callgraph.fieldbased.flowgraph.vertices.VertexFactory;
import com.ibm.wala.cast.js.ipa.callgraph.JSAnalysisOptions;
import com.ibm.wala.cast.js.ssa.JavaScriptInvoke;
import com.ibm.wala.cast.js.types.JavaScriptMethods;
import com.ibm.wala.ipa.callgraph.AnalysisOptions;
import com.ibm.wala.ipa.callgraph.IAnalysisCacheView;
import com.ibm.wala.ipa.cha.IClassHierarchy;
import com.ibm.wala.util.CancelException;
import com.ibm.wala.util.MonitorUtil;
import com.ibm.wala.util.MonitorUtil.IProgressMonitor;
import com.ibm.wala.util.collections.HashSetFactory;
import com.ibm.wala.util.collections.Pair;
import com.ibm.wala.util.intset.OrdinalSet;
import java.util.Set;

/**
 * Optimistic call graph builder that propagates inter-procedural data flow iteratively as call
 * edges are discovered. Slower, but potentially more sound than {@link
 * PessimisticCallGraphBuilder}.
 *
 * @author mschaefer
 */
public class OptimisticCallgraphBuilder extends FieldBasedCallGraphBuilder {
  /** The maximum number of iterations to perform. */
  public int ITERATION_CUTOFF = Integer.MAX_VALUE;

  private final boolean handleCallApply;

  public OptimisticCallgraphBuilder(
      IClassHierarchy cha,
      AnalysisOptions options,
      IAnalysisCacheView iAnalysisCacheView,
      boolean supportFullPointerAnalysis) {
    super(cha, options, iAnalysisCacheView, supportFullPointerAnalysis);
    handleCallApply =
        options instanceof JSAnalysisOptions && ((JSAnalysisOptions) options).handleCallApply();
  }

  @Override
  public FlowGraph buildFlowGraph(IProgressMonitor monitor) throws CancelException {
    FlowGraph flowgraph = flowGraphFactory();

    // keep track of which call edges we already know about
    Set<Pair<CallVertex, FuncVertex>> knownEdges = HashSetFactory.make();

    // flag for fixpoint iteration
    boolean changed = true;
    int iter = 0;

    while (iter++ < ITERATION_CUTOFF && changed) {
      MonitorUtil.throwExceptionIfCanceled(monitor);
      changed = false;

      // extract all call edges from the flow graph
      Set<Pair<CallVertex, FuncVertex>> newEdges = this.extractCallGraphEdges(flowgraph, monitor);

      for (Pair<CallVertex, FuncVertex> edge : newEdges) {
        MonitorUtil.throwExceptionIfCanceled(monitor);

        // set changed to true if this is a new edge
        boolean newEdge = knownEdges.add(edge);
        changed = changed || newEdge;

        if (newEdge) {
          // handle it
          addEdge(flowgraph, edge.fst, edge.snd);

          // special handling of invocations of Function.prototype.call
          // TODO: since we've just added some edges to the flow graph, its transitive closure will
          // be
          //       recomputed here, which is slow and unnecessary
          if (handleCallApply
              && (edge.snd.getFullName().equals("Lprologue.js/Function_prototype_call")
                  || edge.snd.getFullName().equals("Lprologue.js/Function_prototype_apply"))) {
            addReflectiveCallEdge(flowgraph, edge.fst, monitor);
          }
        }
      }
    }

    return flowgraph;
  }

  // add flow corresponding to a new call edge
  private static void addEdge(FlowGraph flowgraph, CallVertex c, FuncVertex callee) {
    VertexFactory factory = flowgraph.getVertexFactory();
    JavaScriptInvoke invoke = c.getInstruction();
    FuncVertex caller = c.getCaller();

    int offset = 0;
    if (invoke
        .getDeclaredTarget()
        .getSelector()
        .equals(JavaScriptMethods.ctorReference.getSelector())) {
      offset = 1;
    }

    for (int i = 0; i < invoke.getNumberOfPositionalParameters(); ++i) {
      // only flow receiver into 'this' if invoke is, in fact, a method call
      flowgraph.addEdge(
          factory.makeVarVertex(caller, invoke.getUse(i)), factory.makeArgVertex(callee));
      // if(i != 1 ||
      // !invoke.getDeclaredTarget().getSelector().equals(AstMethodReference.fnSelector))
      flowgraph.addEdge(
          factory.makeVarVertex(caller, invoke.getUse(i)),
          factory.makeParamVertex(callee, i + offset));
    }

    // flow from return vertex to result vertex
    flowgraph.addEdge(
        factory.makeRetVertex(callee), factory.makeVarVertex(caller, invoke.getDef()));
  }

  // add data flow corresponding to a reflective invocation via Function.prototype.call
  // NB: for f.call(...), f will _not_ appear as a call target, but the appropriate argument and
  // return data flow will be set up
  private static void addReflectiveCallEdge(
      FlowGraph flowgraph, CallVertex c, IProgressMonitor monitor) throws CancelException {
    VertexFactory factory = flowgraph.getVertexFactory();
    FuncVertex caller = c.getCaller();
    JavaScriptInvoke invoke = c.getInstruction();

    VarVertex receiverVertex = factory.makeVarVertex(caller, invoke.getUse(1));
    OrdinalSet<FuncVertex> realCallees = flowgraph.getReachingSet(receiverVertex, monitor);
    System.err.println("callees " + realCallees + " for " + caller);
    for (FuncVertex realCallee : realCallees) {
      // flow from arguments to parameters
      for (int i = 2; i < invoke.getNumberOfPositionalParameters(); ++i)
        flowgraph.addEdge(
            factory.makeVarVertex(caller, invoke.getUse(i)),
            factory.makeParamVertex(realCallee, i - 1));

      // flow from return vertex to result vertex
      flowgraph.addEdge(
          factory.makeRetVertex(realCallee), factory.makeVarVertex(caller, invoke.getDef()));
    }
  }
}
